package de.tuhh.james.weka;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.tuhh.james.weka.exception.InitializationException;

import weka.classifiers.Evaluation;
import weka.core.Attribute;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.SerializationHelper;
import weka.core.converters.ArffSaver;
import weka.core.converters.ConverterUtils.DataSource;
import weka.classifiers.Classifier;
import weka.filters.Filter;

public class ClassfierUtils {
	
	private DataSource trainSource = null;
	private Instances trainData = null;
	private DataSource testSource = null;
	private Instances testData = null;
	private Evaluation evaluation = null;
	private Attribute classLabels = null;
	private Classifier  classifier = null;
	private long buildModelStart = 0l;
	private long buildModelElapsed = 0l;
	private List<Filter> filterList = new ArrayList<Filter>();
	private static ArffSaver saver = new ArffSaver();
	
	private static Logger logger =  LoggerFactory.getLogger(ClassfierUtils.class);

	public Evaluation getEvaluation() {
		return evaluation;
	}

	public Attribute getClassLabels() {
		return classLabels;
	}

	public Classifier getClassifier() {
		return classifier;
	}
	
	public Instances getTestData() {
		return testData;
	}

	public void addFilter(Filter filter){
		filterList.add(filter);
	}

	public void setTrainingDataFile(File inputDataFile) throws Exception{
		logger.debug("Reading the new file for training classifier.");
		trainSource = new DataSource(inputDataFile.getAbsolutePath());
		trainData = trainSource.getDataSet();
		if (trainData.classIndex() == -1)
			   trainData.setClassIndex(trainData.numAttributes() - 1);
		classLabels = trainData.classAttribute();
		if(filterList.size()>0){
			for(Filter filter:filterList){
				filter.setInputFormat(trainData);
				trainData = Filter.useFilter(trainData, filter);
			}
		}
//		saveInstancesToFile(trainData, new File(inputDataFile.getParent(),"filteedTrainData.arff"));
	}
	
	public void setTestingDataFile(File testDataFile) throws Exception{
		logger.debug("Reading the new file for testing classifier.");
		testSource = new DataSource(testDataFile.getAbsolutePath());
		testData = testSource.getDataSet();
		if (testData.classIndex() == -1)
			testData.setClassIndex(testData.numAttributes() - 1);
		if(filterList.size()>0){
			for(Filter filter:filterList){
				filter.setInputFormat(testData);
				testData = Filter.useFilter(testData, filter);
			}
		}
//		saveInstancesToFile(testData, new File(testDataFile.getParent(),"filteedTestData.arff"));
	}
	
	public  void setClassifier(Class<? extends Classifier> wekaClassifierClass,String[] options) throws Exception{
		classifier = wekaClassifierClass.newInstance();
		classifier.setOptions(options);
		buildModelStart = System.currentTimeMillis();
		classifier.buildClassifier(trainData);
		buildModelElapsed = System.currentTimeMillis() - buildModelStart;
		evaluate();
	}
	
	public void evaluate()throws Exception{
		if(testData==null){
			//logger.debug("Test data is not defined, use original training data set and performing cross validation.");
			evaluation = new Evaluation(trainData);
			evaluation.crossValidateModel(classifier, trainData, 10, new Random(1));
		}else{
			//logger.debug("Test data is has been defined, load for test.");
			evaluation = new Evaluation(testData);
			evaluation.useNoPriors();
			evaluation.evaluateModel(classifier, testData);
		}
	}
	
	
	public double[][] getConfutionMatix() throws Exception{
		if(evaluation == null){
			throw new InitializationException("Evaluation is not initialized.");
		}
		return evaluation.confusionMatrix();
	}
	
	private double[][] getPctConfutionMatrix(double[][] confutionMatrix){
		double[][] pctConfutionMatrix = confutionMatrix.clone();
		int sum = 0;
		for(double[] predictionRow:confutionMatrix){
			for(double prediction:predictionRow){
				sum+=prediction;
			}
		}
		logger.info("The sum of all test cast is - "+sum);
		int row = 0;
		for(double[] predictionRow:pctConfutionMatrix){
			for(int j = 0;j<predictionRow.length;j++){
				if(sum==0){
					pctConfutionMatrix[row][j] = 0;
				}else{
					pctConfutionMatrix[row][j] /=sum;
				}
			}
			row++;
		}
		return pctConfutionMatrix;
	}
	
	public double getMeterError(double[][] distanceMatrix,double[][] pctConfutionMatix)throws Exception{
		double result = 0;
		if(distanceMatrix.length!=pctConfutionMatix.length){
			throw new InitializationException("Disance matrix and confution matrix doesn't fit.");
		}
		for(int i=0;i<pctConfutionMatix.length;i++){
			for(int j=0;j<pctConfutionMatix[i].length;j++){
				if(j>i){
					result+=(pctConfutionMatix[i][j] * distanceMatrix[i][j]);
				}else{
					result+=(pctConfutionMatix[i][j] * distanceMatrix[j][i]);
				}
			}
		}
		return result;
	}
	
	public double[][] getPctConfutionMatrix() throws Exception{
		return getPctConfutionMatrix(getConfutionMatix());
	}
	
	public ClassifyResult classifiy(Instance instance)throws Exception{
		if(classifier==null){
			throw new InitializationException("Classifier is not initialized.");
		}
		double classLabel = classifier.classifyInstance(instance);
		String classLableString = classLabels.value((int)classLabel);
		double[] confidences = classifier.distributionForInstance(instance);
		double confidence = confidences[(int) classLabel];
		return new ClassifyResult(classLableString, classLabel,confidence);
	}
	
	/**
	 * return the time elapsed to build the model, unit is second
	 * @return a String of time used to build the model, unit is second.
	 * @throws Exception
	 */
	public String getBuildModelTime(){
		return doubleToString(buildModelElapsed / 1000d, 3);
	}
	
	public double getPctCorretion()throws Exception{
		if(evaluation == null){
			throw new InitializationException("Evaluation is not initialized.");
		}
		return evaluation.pctCorrect();
	}
	
	/**
	 * Generate model from existing classifier.
	 * @param modelFile The output file that the model will be written to.
	 * @throws Exception
	 */
	public void getModel(File modelFile)throws Exception{
		if(classifier==null){
			throw new InitializationException("Classifier is not initialized.");
		}
		SerializationHelper.write(modelFile.getAbsolutePath(), classifier);
	}
	
	public void setClassifierFromModel(File modelFile)throws Exception{
		classifier = (Classifier) SerializationHelper.read(modelFile.getAbsolutePath());
	}
	
	 /**
	   * Rounds a double and converts it into String.
	   *
	   * @param value the double value
	   * @param afterDecimalPoint the (maximum) number of digits permitted
	   * after the decimal point
	   * @return the double as a formatted string
	   */
	  public static /*@pure@*/ String doubleToString(double value, int afterDecimalPoint) {
	    
	    StringBuffer stringBuffer;
	    double temp;
	    int dotPosition;
	    long precisionValue;
	    
	    temp = value * Math.pow(10.0, afterDecimalPoint);
	    if (Math.abs(temp) < Long.MAX_VALUE) {
	      precisionValue = 	(temp > 0) ? (long)(temp + 0.5) 
	                                   : -(long)(Math.abs(temp) + 0.5);
	      if (precisionValue == 0) {
		stringBuffer = new StringBuffer(String.valueOf(0));
	      } else {
		stringBuffer = new StringBuffer(String.valueOf(precisionValue));
	      }
	      if (afterDecimalPoint == 0) {
		return stringBuffer.toString();
	      }
	      dotPosition = stringBuffer.length() - afterDecimalPoint;
	      while (((precisionValue < 0) && (dotPosition < 1)) ||
		     (dotPosition < 0)) {
		if (precisionValue < 0) {
		  stringBuffer.insert(1, '0');
		} else {
		  stringBuffer.insert(0, '0');
		}
		dotPosition++;
	      }
	      stringBuffer.insert(dotPosition, '.');
	      if ((precisionValue < 0) && (stringBuffer.charAt(1) == '.')) {
		stringBuffer.insert(1, '0');
	      } else if (stringBuffer.charAt(0) == '.') {
		stringBuffer.insert(0, '0');
	      }
	      int currentPos = stringBuffer.length() - 1;
	      while ((currentPos > dotPosition) &&
		     (stringBuffer.charAt(currentPos) == '0')) {
		stringBuffer.setCharAt(currentPos--, ' ');
	      }
	      if (stringBuffer.charAt(currentPos) == '.') {
		stringBuffer.setCharAt(currentPos, ' ');
	      }
	      
	      return stringBuffer.toString().trim();
	    }
	    return new String("" + value);
	  }
	  
	 public static double getAverage(List<Double> doubleList){
			double pctSum = 0d;
			double avePctCorrection = 0d;
			for(double d:doubleList){
				pctSum+=d;
			}
			avePctCorrection = pctSum / doubleList.size();
			return avePctCorrection;
		}
		
	public static double getStDvi(List<Double> doubleList){
			double result = 0;
			double average = getAverage(doubleList);
			double sum = 0;
			for(double d:doubleList){
				sum+=((d - average)*(d-average));
			}
			result = Math.sqrt(sum);
			return result;
		}

	public void saveInstancesToFile(Instances instances,File arffFile)throws Exception{
		logger.debug("Try to save instances to file" +arffFile.getName());
		 saver.setInstances(instances);
		 saver.setFile(arffFile);
		 saver.writeBatch();
	}

}
